import { LockManager } from "./defination"
import { ActivityRecord,  ExecutionRecord, LockRecord } from "./entity"
import { Store, VersionedStore,Record,EntityIdentifier,UserIdentifier, DefaultVersionedStore, DefaultStore, StoreTransactionManager, defaultStoreTransactionManager, UniqueIdentifier, EnumField } from "./store"

export class RepositoryStateError extends Error {
  constructor(message:string,public expected:string,public actual:string,public execution:ExecutionRecord) {
    super(message)
  }
}

export class RepositoryLockError extends Error {
  constructor(message:string,public execution:ExecutionRecord,public now?:Date) {
    super(message)
  }
}

export interface ActivityRepository {
  
  /**
   * Begin an activity execution
   * - insert the execution record to db
   * - update the activity if the requestId is undefined or null
   */
  beginActivity(activity: ActivityRecord, execution: ExecutionRecord,requestId:UniqueIdentifier): Promise<ExecutionRecord>

  /**
   * Enter an activity
   * - update the activity
   * - update the execution
   */
  enterActivity(activity: ActivityRecord, execution: ExecutionRecord,initState:EnumField,requestId:UniqueIdentifier): Promise<void>

  /**
   * Move activity to dealing stage
   * - update the activity
   * - update the execution
   */
  dealingActivity(activity: ActivityRecord, execution: ExecutionRecord,prevState:{entered:EnumField,suspended:EnumField},requestId:UniqueIdentifier): Promise<void>

  /**
   * Suspend an activity
   * - update the activity
   * - update the execution
   */
  suspendActivity(activity: ActivityRecord, execution: ExecutionRecord,dealingState:EnumField,requestId:UniqueIdentifier): Promise<void>

  /**
   * Complete an activity
   * - update the activity
   * - update the execution
   */
  doneActivity(activity: ActivityRecord, execution: ExecutionRecord,dealingState:EnumField,requestId:UniqueIdentifier): Promise<void>

  /**
   * Exit an activity
   * - update the activity
   * - update the execution
   */
  exitActivity(activity: ActivityRecord, execution: ExecutionRecord,doneState:EnumField,requestId:UniqueIdentifier): Promise<void>

  /**
   * Halt an activity
   * - update the activity
   * - update the execution
   */
  errorActivity(activity: ActivityRecord, execution: ExecutionRecord,requestId:UniqueIdentifier): Promise<void>

  /**
   * End an activity
   * - update the activity
   * - update the execution
   */
  endActivity(activity: ActivityRecord, execution: ExecutionRecord,requestId:UniqueIdentifier): Promise<void>

}


// general update fields for activity and execution
const statusFields = ['activityStatus','updateTime']
const execFields = ['executeStatus','updateTime']
const lockFields = ['executeStatus','requestId','dealerId','lockTime','lockExpiryTime','updateTime']
const aLockFields = [...lockFields,'executionId']
const overFields = [...execFields,'overTime']
type ActivityFieldName = keyof ActivityRecord
type ExecutionFieldName = keyof ExecutionRecord

export function clearLock(record:LockRecord) {
  record.requestId = undefined
  record.dealerId = undefined
  record.lockTime = undefined
  record.lockExpiryTime = undefined
}

/**
 * Default implementation of ActivityRepository
 * update the activity and execution fields in the same transaction
 * @export
 * @class DefaultActivityRepository
 * @implements {ActivityRepository}
 */
export class DefaultActivityRepository implements ActivityRepository {
  private activityStore:VersionedStore<ActivityRecord>
  private executionStore:Store<ExecutionRecord>
  private transactionManager:StoreTransactionManager
  
  constructor(activityStore?:VersionedStore<ActivityRecord>, executionStore?:Store<ExecutionRecord>,transactionManager?:StoreTransactionManager) {
    if (activityStore) {
      if (!executionStore) {
        throw new Error('Execution repository is required')
      }
      this.activityStore = activityStore
      this.executionStore = executionStore
      this.transactionManager = transactionManager??activityStore.transactionManager
    }else {
      this.transactionManager = transactionManager = transactionManager?? defaultStoreTransactionManager
      this.activityStore = new DefaultVersionedStore<ActivityRecord>([],transactionManager)
      this.executionStore = new DefaultStore<ExecutionRecord>([],transactionManager)
    }
  }

  
  /**
   * begin to execute an activity
   * - create a execution
      - lock fields as the same as the activity
      - struct fields like flowId,graphId,name,path,parentId,activityId,originalActivityStatus as the same as the activity
      - startTime,createTime,creatorId
   * - update the activity set lock fields:
   *  - only allow the assigned request(marked by requestId) to update
   *  - update the lockExpiryTime this field will be checked for the following update

   * @param {ActivityRecord} activity
   * @param {ExecutionRecord} execution
   * @param {UniqueIdentifier} requestId
   * @return {*}  {Promise<ExecutionRecord>}
   * @memberof DefaultActivityRepository
   */
  async beginActivity(activity: ActivityRecord, execution: ExecutionRecord,requestId:UniqueIdentifier): Promise<ExecutionRecord> {
    if (!requestId) {
      throw new Error('requestId is required')
    }
    await this.transactionManager.tranBegin()
    try{
      const aFields:ActivityFieldName[] = [
        ...aLockFields as ActivityFieldName[]
      ]
      const eFields:ExecutionFieldName[] = [
        ...statusFields as ExecutionFieldName[],
        ...execFields as ExecutionFieldName[],
        'graphId','name','path','parentId','flowId','activityId','originalActivityStatus',
        'startTime'
      ]
      this.executionStore.add(execution, eFields)
      activity.executionId = execution.id
      activity.requestId = requestId
      const updatedActivity = await this.activityStore.save(activity, aFields,(existed)=>existed.requestId===undefined || existed.requestId===null)
      if (updatedActivity===undefined) {
        throw new RepositoryLockError('cannot lock the activity' + activity.id,execution)
      }
      await this.transactionManager.tranCommit()
      return execution
    }catch(e){
      await this.transactionManager.tranRollback()
      throw e
    }
  }


  /**
   * enter an activity
   * - check the lock(requestId,lockExpiryTime)
   * - check the activityStatus(initState)
   * = check recordVersion
   * - update the activity : activityStatus, enteredTime and audit fields
   * - update the execution set fields: activityStatus
   * @param {ActivityRecord} activity
   * @param {ExecutionRecord} execution
   * @param {EnumField} initState
   * @param {UniqueIdentifier} requestId
   * @param {Date} now
   * @return {*}  {Promise<void>}
   * @memberof DefaultActivityRepository
   */
  async enterActivity(activity: ActivityRecord, execution: ExecutionRecord,initState:EnumField,requestId:UniqueIdentifier): Promise<void> {
    await this.transactionManager.tranBegin()
    try{
      if(await this.activityStore.save(activity, 
        [...statusFields,'enterTime'] as ActivityFieldName[],
        (existed)=>existed.requestId==requestId && existed.activityStatus==initState
      )) {
        await this.executionStore.save(execution, statusFields as ExecutionFieldName[])
      }else {
        throw new RepositoryLockError('lose ActivityLock',execution)
      }
      await this.transactionManager.tranCommit()
    }catch(e) {
      await this.transactionManager.tranRollback()
      throw e
    }

  }

  
  /**
   * dealing an activity
   * - check the lock(requestId,lockExpiryTime)
   * - check the activityStatus(prevState.entered or prevState.suspended)
   * = check recordVersion
   * - update the activity : activityStatus, dealTime, input, variable and audit fields
   * - update the execution set fields: activityStatus, input, variable
   * @param {ActivityRecord} activity
   * @param {ExecutionRecord} execution
   * @param {{entered:EnumField,suspended:EnumField}} prevState
   * @param {UniqueIdentifier} requestId
   * @param {Date} now
   * @return {*}  {Promise<void>}
   * @memberof DefaultActivityRepository
   */
  async dealingActivity(activity: ActivityRecord, execution: ExecutionRecord,prevState:{entered:EnumField,suspended:EnumField},requestId:UniqueIdentifier): Promise<void> {
    await this.transactionManager.tranBegin() 
    try{
      if(await this.activityStore.save(activity, 
        [...statusFields,'dealTime','input','variable'] as ActivityFieldName[],
        (existed)=> existed.requestId==requestId && (existed.activityStatus==prevState.entered || existed.activityStatus==prevState.suspended)
      )) {
        await this.executionStore.save(execution, [...statusFields,'input','variable'] as ExecutionFieldName[])
      }else {
        throw new RepositoryLockError('lose ActivityLock',execution)
      }
      await this.transactionManager.tranCommit()
    }catch(e) {
      await this.transactionManager.tranRollback()
      throw e
    }
  }
  
  /**
   * suspend an activity
   * - check the lock(requestId,lockExpiryTime)
   * - check the activityStatus(dealingState)
   * = check recordVersion
   * - update the activity : activityStatus, suspendTime, output, error and audit fields
   * - update the execution set fields: activityStatus, output, error
   * @param {ActivityRecord} activity
   * @param {ExecutionRecord} execution
   * @param {EnumField} dealingState
   * @param {UniqueIdentifier} requestId
   * @param {Date} now
   * @return {*}  {Promise<void>}
   * @memberof DefaultActivityRepository
   */
  async suspendActivity(activity: ActivityRecord, execution: ExecutionRecord,dealingState:EnumField,requestId:UniqueIdentifier): Promise<void> {
    await this.transactionManager.tranBegin()
    try{
      if(await this.activityStore.save(
        activity, 
        [
          'activityStatus',
          ...aLockFields,
          'output','error','suspendTime'
        ] as ActivityFieldName[],
        (existed)=>{
          return existed.activityStatus == dealingState && existed.requestId==requestId
        }
      )) {
        await this.executionStore.save(
          execution,
          [
            ...statusFields,
            ...overFields,
            'output','error','overTime'
          ] as ExecutionFieldName[]
          )
      }else {
        throw new RepositoryLockError('lose ActivityLock',execution)
      }
      await this.transactionManager.tranCommit()
    }catch(e) {
      await this.transactionManager.tranRollback()
      throw e
    }
  }
  
  /**
   * done an activity
   * - check the lock(requestId,lockExpiryTime)
   * - check the activityStatus(dealingState)
   * = check recordVersion
   * - update the activity : activityStatus, doneTime, output and audit fields
   * - update the execution set fields: activityStatus, output
   * @param {ActivityRecord} activity
   * @param {ExecutionRecord} execution
   * @param {EnumField} dealingState
   * @param {UniqueIdentifier} requestId
   * @param {Date} now
   * @return {*}  {Promise<void>}
   * @memberof DefaultActivityRepository
   */
  async doneActivity(activity: ActivityRecord, execution: ExecutionRecord,dealingState:EnumField,requestId:UniqueIdentifier): Promise<void> {
    await this.transactionManager.tranBegin()
    try{
      if (await this.activityStore.save(activity, 
        [...statusFields,'output','doneTime'] as ActivityFieldName[],
        (existed)=> existed.activityStatus == dealingState && existed.requestId==activity.requestId
      )){
        this.executionStore.save(execution, [...statusFields,'output'] as ExecutionFieldName[])
      }else {
        throw new RepositoryLockError('lose ActivityLock',execution)
      }
      await this.transactionManager.tranCommit()
    }catch(e) {
      await this.transactionManager.tranRollback()
      throw e
    }
  }
  
  /**
   * exit an activity
   * - check the lock(requestId,lockExpiryTime)
   * - check the activityStatus(doneState)
   * = check recordVersion
   * - update the activity : activityStatus, exitTime, lock fields and audit fields
   * - update the execution set fields: activityStatus, overTime
   */ 
  async exitActivity(activity: ActivityRecord, execution: ExecutionRecord,doneState:EnumField,requestId:UniqueIdentifier): Promise<void> {
    await this.transactionManager.tranBegin()
    try{
      
      if( await this.activityStore.save(activity, 
        [...statusFields,...aLockFields] as ActivityFieldName[],
        (existed)=> existed.requestId==requestId && existed.activityStatus==doneState
      )) {
        await this.executionStore.save(execution, [...statusFields,...overFields] as ExecutionFieldName[])
      }else {
        throw new RepositoryLockError('lose ActivityLock',execution)
      }
      await this.transactionManager.tranCommit()
    }catch(e) {
      await this.transactionManager.tranRollback()
      throw e
    }
  }

  
  /**
   * error an activity
   * - update the activity : error
   * - update the execution set fields: error
   * 
   */ 
  async errorActivity(activity: ActivityRecord, execution: ExecutionRecord,requestId:UniqueIdentifier): Promise<void> {
    await this.transactionManager.tranBegin()
    try{
      await this.executionStore.save(execution, [...overFields,'error'] as ExecutionFieldName[])

      await this.activityStore.save(activity, 
        [...aLockFields,'error'] as ActivityFieldName[],
        (existed)=> existed.requestId==requestId
      )
      
      await this.transactionManager.tranCommit()
    }catch(e) {
      await this.transactionManager.tranRollback()
      throw e
    }
  }
  async endActivity(activity: ActivityRecord, execution: ExecutionRecord,requestId:UniqueIdentifier): Promise<void> {
    await this.transactionManager.tranBegin()
    try{  
      await this.executionStore.save(
        execution, 
        overFields as ExecutionFieldName[])
      
      await this.activityStore.save(activity, 
        aLockFields as ActivityFieldName[],
         (existed)=>existed.requestId=== requestId
      )
      await this.transactionManager.tranCommit()
    }catch(e) {
      await this.transactionManager.tranRollback()
      throw e
    }
  }
} 
